const std = @import("std");
const builtin = @import("builtin");
const Allocator = std.mem.Allocator;
const cimgui = @import("cimgui");
const OptionAsAlt = @import("config.zig").OptionAsAlt;

/// A generic key input event. This is the information that is necessary
/// regardless of apprt in order to generate the proper terminal
/// control sequences for a given key press.
///
/// Some apprts may not be able to provide all of this information, such
/// as GLFW. In this case, the apprt should provide as much information
/// as it can and it should be expected that the terminal behavior
/// will not be totally correct.
pub const KeyEvent = struct {
    /// The action: press, release, etc.
    action: Action = .press,

    /// The keycode of the physical key that was pressed. This is agnostic
    /// to the layout. Layout-dependent matching can only be done via the
    /// UTF-8 or unshifted codepoint.
    key: Key = .unidentified,

    /// Mods are the modifiers that are pressed.
    mods: Mods = .{},

    /// The mods that were consumed in order to generate the text
    /// in utf8. This has the mods set that were consumed, so to
    /// get the set of mods that are effective you must negate
    /// mods with this.
    ///
    /// This field is meaningless if utf8 is empty.
    consumed_mods: Mods = .{},

    /// Composing is true when this key event is part of a dead key
    /// composition sequence and we're in the middle of it.
    composing: bool = false,

    /// The utf8 sequence that was generated by this key event.
    /// This will be an empty string if there is no text generated.
    /// If composing is true and this is non-empty, this is preedit
    /// text.
    utf8: []const u8 = "",

    /// The codepoint for this key when it is unshifted. For example,
    /// shift+a is "A" in UTF-8 but unshifted would provide 'a'.
    unshifted_codepoint: u21 = 0,

    /// Returns the effective modifiers for this event. The effective
    /// modifiers are the mods that should be considered for keybindings.
    pub fn effectiveMods(self: KeyEvent) Mods {
        if (self.utf8.len == 0) return self.mods;
        return self.mods.unset(self.consumed_mods);
    }

    /// Returns a unique hash for this key event to be used for tracking
    /// uniquess specifically with bindings. This omits fields that are
    /// irrelevant for bindings.
    pub fn bindingHash(self: KeyEvent) u64 {
        var hasher = std.hash.Wyhash.init(0);

        // These are all the fields that are explicitly part of Trigger.
        std.hash.autoHash(&hasher, self.key);
        std.hash.autoHash(&hasher, self.unshifted_codepoint);
        std.hash.autoHash(&hasher, self.mods.binding());

        // Notes on unmapped things and why:
        //
        // - action: we don't have action-specific bindings right now
        //   AND we want to know if a key resulted in a binding regardless
        //   of action because a press should also ignore a release and so on.
        //
        // We can add to this if there is other confusion.

        return hasher.final();
    }
};

/// A bitmask for all key modifiers.
///
/// IMPORTANT: Any changes here update include/ghostty.h
pub const Mods = packed struct(Mods.Backing) {
    pub const Backing = u16;

    shift: bool = false,
    ctrl: bool = false,
    alt: bool = false,
    super: bool = false,
    caps_lock: bool = false,
    num_lock: bool = false,
    sides: side = .{},
    _padding: u6 = 0,

    /// Tracks the side that is active for any given modifier. Note
    /// that this doesn't confirm a modifier is pressed; you must check
    /// the bool for that in addition to this.
    ///
    /// Not all platforms support this, check apprt for more info.
    pub const side = packed struct(u4) {
        shift: Side = .left,
        ctrl: Side = .left,
        alt: Side = .left,
        super: Side = .left,
    };

    pub const Side = enum(u1) { left, right };

    /// Integer value of this struct.
    pub fn int(self: Mods) Backing {
        return @bitCast(self);
    }

    /// Returns true if no modifiers are set.
    pub fn empty(self: Mods) bool {
        return self.int() == 0;
    }

    /// Returns true if two mods are equal.
    pub fn equal(self: Mods, other: Mods) bool {
        return self.int() == other.int();
    }

    /// Return mods that are only relevant for bindings.
    pub fn binding(self: Mods) Mods {
        return .{
            .shift = self.shift,
            .ctrl = self.ctrl,
            .alt = self.alt,
            .super = self.super,
        };
    }

    /// Perform `self &~ other` to remove the other mods from self.
    pub fn unset(self: Mods, other: Mods) Mods {
        return @bitCast(self.int() & ~other.int());
    }

    /// Returns the mods without locks set.
    pub fn withoutLocks(self: Mods) Mods {
        var copy = self;
        copy.caps_lock = false;
        copy.num_lock = false;
        return copy;
    }

    /// Return the mods to use for key translation. This handles settings
    /// like macos-option-as-alt. The translation mods should be used for
    /// translation but never sent back in for the key callback.
    pub fn translation(self: Mods, option_as_alt: OptionAsAlt) Mods {
        var result = self;

        // macos-option-as-alt for darwin
        if (comptime builtin.target.os.tag.isDarwin()) alt: {
            // Alt has to be set only on the correct side
            switch (option_as_alt) {
                .false => break :alt,
                .true => {},
                .left => if (self.sides.alt == .right) break :alt,
                .right => if (self.sides.alt == .left) break :alt,
            }

            // Unset alt
            result.alt = false;
        }

        return result;
    }

    /// Checks to see if super is on (MacOS) or ctrl.
    pub fn ctrlOrSuper(self: Mods) bool {
        if (comptime builtin.target.os.tag.isDarwin()) {
            return self.super;
        }
        return self.ctrl;
    }

    // For our own understanding
    test {
        const testing = std.testing;
        try testing.expectEqual(@as(Backing, @bitCast(Mods{})), @as(Backing, 0b0));
        try testing.expectEqual(
            @as(Backing, @bitCast(Mods{ .shift = true })),
            @as(Backing, 0b0000_0001),
        );
    }

    test "translation macos-option-as-alt" {
        if (comptime !builtin.target.os.tag.isDarwin()) return error.SkipZigTest;

        const testing = std.testing;

        // Unset
        {
            const mods: Mods = .{};
            const result = mods.translation(.true);
            try testing.expectEqual(result, mods);
        }

        // Set
        {
            const mods: Mods = .{ .alt = true };
            const result = mods.translation(.true);
            try testing.expectEqual(Mods{}, result);
        }

        // Set but disabled
        {
            const mods: Mods = .{ .alt = true };
            const result = mods.translation(.false);
            try testing.expectEqual(result, mods);
        }

        // Set wrong side
        {
            const mods: Mods = .{ .alt = true, .sides = .{ .alt = .right } };
            const result = mods.translation(.left);
            try testing.expectEqual(result, mods);
        }
        {
            const mods: Mods = .{ .alt = true, .sides = .{ .alt = .left } };
            const result = mods.translation(.right);
            try testing.expectEqual(result, mods);
        }

        // Set with other mods
        {
            const mods: Mods = .{ .alt = true, .shift = true };
            const result = mods.translation(.true);
            try testing.expectEqual(Mods{ .shift = true }, result);
        }
    }
};

/// The action associated with an input event. This is backed by a c_int
/// so that we can use the enum as-is for our embedding API.
///
/// IMPORTANT: Any changes here update include/ghostty.h
pub const Action = enum(c_int) {
    release,
    press,
    repeat,
};

/// The set of key codes that Ghostty is aware of. These represent
/// physical keys on the keyboard. The logical key (or key string)
/// is the string that is generated by the key event and that is up
/// to the apprt to provide.
///
/// Note that these are layout-independent. For example, the "a"
/// key on a US keyboard is the same as the "ф" key on a Russian
/// keyboard, but both will report the "a" enum value in the key
/// event. These values are based on the W3C standard. See:
/// https://www.w3.org/TR/uievents-code
///
/// Layout-dependent strings are provided in the KeyEvent struct as
/// UTF-8 and are produced by the associated apprt. Ghostty core has
/// no mechanism to map input events to strings without the apprt.
///
/// IMPORTANT: Any changes here update include/ghostty.h ghostty_input_key_e
pub const Key = enum(c_int) {
    unidentified,

    // "Writing System Keys" § 3.1.1
    backquote,
    backslash,
    bracket_left,
    bracket_right,
    comma,
    digit_0,
    digit_1,
    digit_2,
    digit_3,
    digit_4,
    digit_5,
    digit_6,
    digit_7,
    digit_8,
    digit_9,
    equal,
    intl_backslash,
    intl_ro,
    intl_yen,
    key_a,
    key_b,
    key_c,
    key_d,
    key_e,
    key_f,
    key_g,
    key_h,
    key_i,
    key_j,
    key_k,
    key_l,
    key_m,
    key_n,
    key_o,
    key_p,
    key_q,
    key_r,
    key_s,
    key_t,
    key_u,
    key_v,
    key_w,
    key_x,
    key_y,
    key_z,
    minus,
    period,
    quote,
    semicolon,
    slash,

    // "Functional Keys" § 3.1.2
    alt_left,
    alt_right,
    backspace,
    caps_lock,
    context_menu,
    control_left,
    control_right,
    enter,
    meta_left,
    meta_right,
    shift_left,
    shift_right,
    space,
    tab,
    convert,
    kana_mode,
    non_convert,

    // "Control Pad Section" § 3.2
    delete,
    end,
    help,
    home,
    insert,
    page_down,
    page_up,

    // "Arrow Pad Section" § 3.3
    arrow_down,
    arrow_left,
    arrow_right,
    arrow_up,

    // "Numpad Section" § 3.4
    num_lock,
    numpad_0,
    numpad_1,
    numpad_2,
    numpad_3,
    numpad_4,
    numpad_5,
    numpad_6,
    numpad_7,
    numpad_8,
    numpad_9,
    numpad_add,
    numpad_backspace,
    numpad_clear,
    numpad_clear_entry,
    numpad_comma,
    numpad_decimal,
    numpad_divide,
    numpad_enter,
    numpad_equal,
    numpad_memory_add,
    numpad_memory_clear,
    numpad_memory_recall,
    numpad_memory_store,
    numpad_memory_subtract,
    numpad_multiply,
    numpad_paren_left,
    numpad_paren_right,
    numpad_subtract,

    // > For numpads that provide keys not listed here, a code value string
    // > should be created by starting with "Numpad" and appending an
    // > appropriate description of the key.
    //
    // These numpad entries are distinguished by various encoding protocols
    // (legacy and Kitty) so we support them here in case the apprt can
    // produce them.
    numpad_separator,
    numpad_up,
    numpad_down,
    numpad_right,
    numpad_left,
    numpad_begin,
    numpad_home,
    numpad_end,
    numpad_insert,
    numpad_delete,
    numpad_page_up,
    numpad_page_down,

    // "Function Section" § 3.5
    escape,
    f1,
    f2,
    f3,
    f4,
    f5,
    f6,
    f7,
    f8,
    f9,
    f10,
    f11,
    f12,
    f13,
    f14,
    f15,
    f16,
    f17,
    f18,
    f19,
    f20,
    f21,
    f22,
    f23,
    f24,
    f25,
    @"fn",
    fn_lock,
    print_screen,
    scroll_lock,
    pause,

    // "Media Keys" § 3.6
    browser_back,
    browser_favorites,
    browser_forward,
    browser_home,
    browser_refresh,
    browser_search,
    browser_stop,
    eject,
    launch_app_1,
    launch_app_2,
    launch_mail,
    media_play_pause,
    media_select,
    media_stop,
    media_track_next,
    media_track_previous,
    power,
    sleep,
    audio_volume_down,
    audio_volume_mute,
    audio_volume_up,
    wake_up,

    // "Legacy, Non-standard, and Special Keys" § 3.7
    copy,
    cut,
    paste,

    /// Converts an ASCII character to a key, if possible. This returns
    /// null if the character is unknown.
    ///
    /// Note that this can't distinguish between physical keys, i.e. '0'
    /// may be from the number row or the keypad, but it always maps
    /// to '.zero'.
    ///
    /// This is what we want, we want people to create keybindings that
    /// are independent of the physical key.
    pub fn fromASCII(ch: u8) ?Key {
        return switch (ch) {
            inline else => |comptime_ch| {
                return comptime result: {
                    @setEvalBranchQuota(100_000);
                    for (codepoint_map) |entry| {
                        // No ASCII characters should ever map to a keypad key
                        if (entry[1].keypad()) continue;

                        if (entry[0] == @as(u21, @intCast(comptime_ch))) {
                            break :result entry[1];
                        }
                    }

                    break :result null;
                };
            },
        };
    }

    /// Converts a W3C key code to a Ghostty key enum value.
    ///
    /// All required W3C key codes are supported, but there are a number of
    /// non-standard key codes that are not supported. In the case the value is
    /// invalid or unsupported, this function will return null.
    pub fn fromW3C(code: []const u8) ?Key {
        var result: [128]u8 = undefined;

        // If the code is bigger than our buffer it can't possibly match.
        if (code.len > result.len) return null;

        // First just check the whole thing lowercased, this is the simple case
        if (std.meta.stringToEnum(
            Key,
            std.ascii.lowerString(&result, code),
        )) |key| return key;

        // We need to convert FooBar to foo_bar
        var fbs = std.io.fixedBufferStream(&result);
        const w = fbs.writer();
        for (code, 0..) |ch, i| switch (ch) {
            'a'...'z' => w.writeByte(ch) catch return null,

            // Caps and numbers trigger underscores
            'A'...'Z', '0'...'9' => {
                if (i > 0) w.writeByte('_') catch return null;
                w.writeByte(std.ascii.toLower(ch)) catch return null;
            },

            // We don't know of any key codes that aren't alphanumeric.
            else => return null,
        };

        return std.meta.stringToEnum(Key, fbs.getWritten());
    }

    /// Converts a Ghostty key enum value to a W3C key code.
    pub fn w3c(self: Key) []const u8 {
        return switch (self) {
            inline else => |tag| comptime w3c: {
                @setEvalBranchQuota(50_000);

                const name = @tagName(tag);

                var buf: [128]u8 = undefined;
                var fbs = std.io.fixedBufferStream(&buf);
                const w = fbs.writer();
                var i: usize = 0;
                while (i < name.len) {
                    if (i == 0) {
                        w.writeByte(std.ascii.toUpper(name[i])) catch unreachable;
                    } else if (name[i] == '_') {
                        i += 1;
                        w.writeByte(std.ascii.toUpper(name[i])) catch unreachable;
                    } else {
                        w.writeByte(name[i]) catch unreachable;
                    }

                    i += 1;
                }

                const written = buf;
                const result = written[0..fbs.getWritten().len];
                break :w3c result;
            },
        };
    }

    /// True if this key represents a printable character.
    pub fn printable(self: Key) bool {
        return switch (self) {
            inline else => |tag| {
                return comptime result: {
                    @setEvalBranchQuota(10_000);
                    for (codepoint_map) |entry| {
                        if (entry[1] == tag) break :result true;
                    }

                    break :result false;
                };
            },
        };
    }

    /// True if this key is a modifier.
    pub fn modifier(self: Key) bool {
        return switch (self) {
            .shift_left,
            .control_left,
            .alt_left,
            .meta_left,
            .shift_right,
            .control_right,
            .alt_right,
            .meta_right,
            => true,

            else => false,
        };
    }

    /// Whether this key should be remappable by the operating system.
    ///
    /// On certain OSes (namely Linux and the BSDs) certain keys like the
    /// functional keys are expected to be remappable by the user, such as
    /// in the very common use case of swapping the Caps Lock key with the
    /// Escape key with the XKB option `caps:swapescape`.
    ///
    /// However, the way XKB implements this is by essentially acting as a
    /// software key remapper that destroys all information about the original
    /// physical key, leading to very annoying bugs like #7309 where the
    /// physical key `XKB_KEY_c` gets remapped into `XKB_KEY_Cyrillic_tse`,
    /// which causes all of our physical key handling to completely break down.
    /// _Very naughty._
    ///
    /// As a compromise, given that writing system keys (§3.1.1) comprise the
    /// majority of keys that "change meaning [...] based on the current locale
    /// and keyboard layout", we allow all other keys to be remapped by default
    /// since they should be fairly harmless. We might consider making this
    /// configurable, but for now this should at least placate most people.
    pub fn shouldBeRemappable(self: Key) bool {
        return switch (self) {
            // "Writing System Keys" § 3.1.1
            .backquote,
            .backslash,
            .bracket_left,
            .bracket_right,
            .comma,
            .digit_0,
            .digit_1,
            .digit_2,
            .digit_3,
            .digit_4,
            .digit_5,
            .digit_6,
            .digit_7,
            .digit_8,
            .digit_9,
            .equal,
            .intl_backslash,
            .intl_ro,
            .intl_yen,
            .key_a,
            .key_b,
            .key_c,
            .key_d,
            .key_e,
            .key_f,
            .key_g,
            .key_h,
            .key_i,
            .key_j,
            .key_k,
            .key_l,
            .key_m,
            .key_n,
            .key_o,
            .key_p,
            .key_q,
            .key_r,
            .key_s,
            .key_t,
            .key_u,
            .key_v,
            .key_w,
            .key_x,
            .key_y,
            .key_z,
            .minus,
            .period,
            .quote,
            .semicolon,
            .slash,
            => false,

            else => true,
        };
    }

    /// Returns true if this is a keypad key.
    pub fn keypad(self: Key) bool {
        return switch (self) {
            inline else => |tag| {
                const name = @tagName(tag);
                const result = comptime std.mem.startsWith(u8, name, "numpad_");
                return result;
            },
        };
    }

    // Returns the codepoint representing this key, or null if the key is not
    // printable
    pub fn codepoint(self: Key) ?u21 {
        return switch (self) {
            inline else => |tag| {
                return comptime result: {
                    @setEvalBranchQuota(10_000);
                    for (codepoint_map) |entry| {
                        if (entry[1] == tag) break :result entry[0];
                    }

                    break :result null;
                };
            },
        };
    }

    /// Returns the cimgui key constant for this key.
    pub fn imguiKey(self: Key) ?c_uint {
        return switch (self) {
            .key_a => cimgui.c.ImGuiKey_A,
            .key_b => cimgui.c.ImGuiKey_B,
            .key_c => cimgui.c.ImGuiKey_C,
            .key_d => cimgui.c.ImGuiKey_D,
            .key_e => cimgui.c.ImGuiKey_E,
            .key_f => cimgui.c.ImGuiKey_F,
            .key_g => cimgui.c.ImGuiKey_G,
            .key_h => cimgui.c.ImGuiKey_H,
            .key_i => cimgui.c.ImGuiKey_I,
            .key_j => cimgui.c.ImGuiKey_J,
            .key_k => cimgui.c.ImGuiKey_K,
            .key_l => cimgui.c.ImGuiKey_L,
            .key_m => cimgui.c.ImGuiKey_M,
            .key_n => cimgui.c.ImGuiKey_N,
            .key_o => cimgui.c.ImGuiKey_O,
            .key_p => cimgui.c.ImGuiKey_P,
            .key_q => cimgui.c.ImGuiKey_Q,
            .key_r => cimgui.c.ImGuiKey_R,
            .key_s => cimgui.c.ImGuiKey_S,
            .key_t => cimgui.c.ImGuiKey_T,
            .key_u => cimgui.c.ImGuiKey_U,
            .key_v => cimgui.c.ImGuiKey_V,
            .key_w => cimgui.c.ImGuiKey_W,
            .key_x => cimgui.c.ImGuiKey_X,
            .key_y => cimgui.c.ImGuiKey_Y,
            .key_z => cimgui.c.ImGuiKey_Z,

            .digit_0 => cimgui.c.ImGuiKey_0,
            .digit_1 => cimgui.c.ImGuiKey_1,
            .digit_2 => cimgui.c.ImGuiKey_2,
            .digit_3 => cimgui.c.ImGuiKey_3,
            .digit_4 => cimgui.c.ImGuiKey_4,
            .digit_5 => cimgui.c.ImGuiKey_5,
            .digit_6 => cimgui.c.ImGuiKey_6,
            .digit_7 => cimgui.c.ImGuiKey_7,
            .digit_8 => cimgui.c.ImGuiKey_8,
            .digit_9 => cimgui.c.ImGuiKey_9,

            .semicolon => cimgui.c.ImGuiKey_Semicolon,
            .space => cimgui.c.ImGuiKey_Space,
            .quote => cimgui.c.ImGuiKey_Apostrophe,
            .comma => cimgui.c.ImGuiKey_Comma,
            .backquote => cimgui.c.ImGuiKey_GraveAccent,
            .period => cimgui.c.ImGuiKey_Period,
            .slash => cimgui.c.ImGuiKey_Slash,
            .minus => cimgui.c.ImGuiKey_Minus,
            .equal => cimgui.c.ImGuiKey_Equal,
            .bracket_left => cimgui.c.ImGuiKey_LeftBracket,
            .bracket_right => cimgui.c.ImGuiKey_RightBracket,
            .backslash => cimgui.c.ImGuiKey_Backslash,

            .arrow_up => cimgui.c.ImGuiKey_UpArrow,
            .arrow_down => cimgui.c.ImGuiKey_DownArrow,
            .arrow_left => cimgui.c.ImGuiKey_LeftArrow,
            .arrow_right => cimgui.c.ImGuiKey_RightArrow,
            .home => cimgui.c.ImGuiKey_Home,
            .end => cimgui.c.ImGuiKey_End,
            .insert => cimgui.c.ImGuiKey_Insert,
            .delete => cimgui.c.ImGuiKey_Delete,
            .caps_lock => cimgui.c.ImGuiKey_CapsLock,
            .scroll_lock => cimgui.c.ImGuiKey_ScrollLock,
            .num_lock => cimgui.c.ImGuiKey_NumLock,
            .page_up => cimgui.c.ImGuiKey_PageUp,
            .page_down => cimgui.c.ImGuiKey_PageDown,
            .escape => cimgui.c.ImGuiKey_Escape,
            .enter => cimgui.c.ImGuiKey_Enter,
            .tab => cimgui.c.ImGuiKey_Tab,
            .backspace => cimgui.c.ImGuiKey_Backspace,
            .print_screen => cimgui.c.ImGuiKey_PrintScreen,
            .pause => cimgui.c.ImGuiKey_Pause,
            .context_menu => cimgui.c.ImGuiKey_Menu,

            .f1 => cimgui.c.ImGuiKey_F1,
            .f2 => cimgui.c.ImGuiKey_F2,
            .f3 => cimgui.c.ImGuiKey_F3,
            .f4 => cimgui.c.ImGuiKey_F4,
            .f5 => cimgui.c.ImGuiKey_F5,
            .f6 => cimgui.c.ImGuiKey_F6,
            .f7 => cimgui.c.ImGuiKey_F7,
            .f8 => cimgui.c.ImGuiKey_F8,
            .f9 => cimgui.c.ImGuiKey_F9,
            .f10 => cimgui.c.ImGuiKey_F10,
            .f11 => cimgui.c.ImGuiKey_F11,
            .f12 => cimgui.c.ImGuiKey_F12,

            .numpad_0 => cimgui.c.ImGuiKey_Keypad0,
            .numpad_1 => cimgui.c.ImGuiKey_Keypad1,
            .numpad_2 => cimgui.c.ImGuiKey_Keypad2,
            .numpad_3 => cimgui.c.ImGuiKey_Keypad3,
            .numpad_4 => cimgui.c.ImGuiKey_Keypad4,
            .numpad_5 => cimgui.c.ImGuiKey_Keypad5,
            .numpad_6 => cimgui.c.ImGuiKey_Keypad6,
            .numpad_7 => cimgui.c.ImGuiKey_Keypad7,
            .numpad_8 => cimgui.c.ImGuiKey_Keypad8,
            .numpad_9 => cimgui.c.ImGuiKey_Keypad9,
            .numpad_decimal => cimgui.c.ImGuiKey_KeypadDecimal,
            .numpad_divide => cimgui.c.ImGuiKey_KeypadDivide,
            .numpad_multiply => cimgui.c.ImGuiKey_KeypadMultiply,
            .numpad_subtract => cimgui.c.ImGuiKey_KeypadSubtract,
            .numpad_add => cimgui.c.ImGuiKey_KeypadAdd,
            .numpad_enter => cimgui.c.ImGuiKey_KeypadEnter,
            .numpad_equal => cimgui.c.ImGuiKey_KeypadEqual,
            // We map KP_SEPARATOR to Comma because traditionally a numpad would
            // have a numeric separator key. Most modern numpads do not
            .numpad_left => cimgui.c.ImGuiKey_LeftArrow,
            .numpad_right => cimgui.c.ImGuiKey_RightArrow,
            .numpad_up => cimgui.c.ImGuiKey_UpArrow,
            .numpad_down => cimgui.c.ImGuiKey_DownArrow,
            .numpad_page_up => cimgui.c.ImGuiKey_PageUp,
            .numpad_page_down => cimgui.c.ImGuiKey_PageUp,
            .numpad_home => cimgui.c.ImGuiKey_Home,
            .numpad_end => cimgui.c.ImGuiKey_End,
            .numpad_insert => cimgui.c.ImGuiKey_Insert,
            .numpad_delete => cimgui.c.ImGuiKey_Delete,
            .numpad_begin => cimgui.c.ImGuiKey_NamedKey_BEGIN,

            .shift_left => cimgui.c.ImGuiKey_LeftShift,
            .control_left => cimgui.c.ImGuiKey_LeftCtrl,
            .alt_left => cimgui.c.ImGuiKey_LeftAlt,
            .meta_left => cimgui.c.ImGuiKey_LeftSuper,
            .shift_right => cimgui.c.ImGuiKey_RightShift,
            .control_right => cimgui.c.ImGuiKey_RightCtrl,
            .alt_right => cimgui.c.ImGuiKey_RightAlt,
            .meta_right => cimgui.c.ImGuiKey_RightSuper,

            // These keys aren't represented in cimgui
            .f13,
            .f14,
            .f15,
            .f16,
            .f17,
            .f18,
            .f19,
            .f20,
            .f21,
            .f22,
            .f23,
            .f24,
            .f25,
            .intl_backslash,
            .intl_ro,
            .intl_yen,
            .convert,
            .kana_mode,
            .non_convert,
            .numpad_separator,
            .numpad_backspace,
            .numpad_clear,
            .numpad_clear_entry,
            .numpad_comma,
            .numpad_memory_add,
            .numpad_memory_clear,
            .numpad_memory_recall,
            .numpad_memory_store,
            .numpad_memory_subtract,
            .numpad_paren_left,
            .numpad_paren_right,
            .@"fn",
            .fn_lock,
            .browser_back,
            .browser_favorites,
            .browser_forward,
            .browser_home,
            .browser_refresh,
            .browser_search,
            .browser_stop,
            .eject,
            .launch_app_1,
            .launch_app_2,
            .launch_mail,
            .media_play_pause,
            .media_select,
            .media_stop,
            .media_track_next,
            .media_track_previous,
            .power,
            .sleep,
            .audio_volume_down,
            .audio_volume_mute,
            .audio_volume_up,
            .wake_up,
            .help,
            .copy,
            .cut,
            .paste,
            => null,

            .unidentified,
            => null,
        };
    }

    /// true if this key is one of the left or right versions of super (MacOS)
    /// or ctrl.
    pub fn ctrlOrSuper(self: Key) bool {
        if (comptime builtin.target.os.tag.isDarwin()) {
            return self == .meta_left or self == .meta_right;
        }
        return self == .control_left or self == .control_right;
    }

    /// true if this key is either left or right shift.
    pub fn leftOrRightShift(self: Key) bool {
        return self == .shift_left or self == .shift_right;
    }

    /// true if this key is either left or right alt.
    pub fn leftOrRightAlt(self: Key) bool {
        return self == .alt_left or self == .alt_right;
    }

    test "fromASCII should not return keypad keys" {
        const testing = std.testing;
        try testing.expect(Key.fromASCII('0').? == .digit_0);
        try testing.expect(Key.fromASCII('*') == null);
    }

    test "keypad keys" {
        const testing = std.testing;
        try testing.expect(Key.numpad_0.keypad());
        try testing.expect(!Key.digit_1.keypad());
    }

    test "w3c" {
        // All our keys should convert to and from the W3C format.
        // We don't support every key in the W3C spec, so we only
        // check the enum fields.
        const testing = std.testing;
        inline for (@typeInfo(Key).@"enum".fields) |field| {
            const key = @field(Key, field.name);
            const w3c_name = key.w3c();
            try testing.expectEqual(key, Key.fromW3C(w3c_name).?);
        }
    }

    const codepoint_map: []const struct { u21, Key } = &.{
        .{ 'a', .key_a },
        .{ 'b', .key_b },
        .{ 'c', .key_c },
        .{ 'd', .key_d },
        .{ 'e', .key_e },
        .{ 'f', .key_f },
        .{ 'g', .key_g },
        .{ 'h', .key_h },
        .{ 'i', .key_i },
        .{ 'j', .key_j },
        .{ 'k', .key_k },
        .{ 'l', .key_l },
        .{ 'm', .key_m },
        .{ 'n', .key_n },
        .{ 'o', .key_o },
        .{ 'p', .key_p },
        .{ 'q', .key_q },
        .{ 'r', .key_r },
        .{ 's', .key_s },
        .{ 't', .key_t },
        .{ 'u', .key_u },
        .{ 'v', .key_v },
        .{ 'w', .key_w },
        .{ 'x', .key_x },
        .{ 'y', .key_y },
        .{ 'z', .key_z },
        .{ '0', .digit_0 },
        .{ '1', .digit_1 },
        .{ '2', .digit_2 },
        .{ '3', .digit_3 },
        .{ '4', .digit_4 },
        .{ '5', .digit_5 },
        .{ '6', .digit_6 },
        .{ '7', .digit_7 },
        .{ '8', .digit_8 },
        .{ '9', .digit_9 },
        .{ ';', .semicolon },
        .{ ' ', .space },
        .{ '\'', .quote },
        .{ ',', .comma },
        .{ '`', .backquote },
        .{ '.', .period },
        .{ '/', .slash },
        .{ '-', .minus },
        .{ '=', .equal },
        .{ '[', .bracket_left },
        .{ ']', .bracket_right },
        .{ '\\', .backslash },

        // Control characters
        .{ '\t', .tab },

        // Keypad entries. We just assume keypad with the numpad_ prefix
        // so that has some special meaning. These must also always be last,
        // so that our `fromASCII` function doesn't accidentally map them
        // over normal numerics and other keys.
        .{ '0', .numpad_0 },
        .{ '1', .numpad_1 },
        .{ '2', .numpad_2 },
        .{ '3', .numpad_3 },
        .{ '4', .numpad_4 },
        .{ '5', .numpad_5 },
        .{ '6', .numpad_6 },
        .{ '7', .numpad_7 },
        .{ '8', .numpad_8 },
        .{ '9', .numpad_9 },
        .{ '.', .numpad_decimal },
        .{ '/', .numpad_divide },
        .{ '*', .numpad_multiply },
        .{ '-', .numpad_subtract },
        .{ '+', .numpad_add },
        .{ '=', .numpad_equal },
    };
};

/// This sets either "ctrl" or "super" to true (but not both)
/// on mods depending on if the build target is Mac or not. On
/// Mac, we default to super (i.e. super+c for copy) and on
/// non-Mac we default to ctrl (i.e. ctrl+c for copy).
pub fn ctrlOrSuper(mods: Mods) Mods {
    var copy = mods;
    if (comptime builtin.target.os.tag.isDarwin()) {
        copy.super = true;
    } else {
        copy.ctrl = true;
    }

    return copy;
}

test "ctrlOrSuper" {
    const testing = std.testing;
    var m: Mods = ctrlOrSuper(.{});

    try testing.expect(m.ctrlOrSuper());
}
